J'ai été très critique sur le code proposé par Collin Damon car en voulant montrer ce qu'est la OOP le seul exemple qu'il donne d'API à se fabriquer est entièrement statique.
Je reprends donc son exemple et je vais tenter de le rendre objet :
public final class Assert {
private Assert() {}
public static void notNull(String fieldName, Object input) {
if (input == null) {
throw MissingMandatoryValueException.forNullValue(fieldName);
}
}
public static void notBlank(String fieldName, String input) {
if (input == null) {
throw MissingMandatoryValueException.forNullValue(fieldName);
}
if (input.isBlank()) {
throw MissingMandatoryValueException.forBlankValue(fieldName);
}
}
public static StringAsserter field(String fieldName, String value) {
return new StringAsserter(fieldName, value);
}
public static class StringAsserter {
private final String fieldName;
private final String value;
private StringAsserter(String fieldName, String value) {
this.fieldName = fieldName;
this.value = value;
}
public StringAsserter notBlank() {
Assert.notBlank(fieldName, value);
return this;
}
public StringAsserter maxLength(int maxLength) {
if (value != null && value.length() > maxLength) {
throw StringSizeExceededException
.builder()
.field(fieldName)
.currentLength(value.length())
.maxLength(maxLength).build();
}
return this;
}
}
}
Étape 1 - Créer une interface qui va exprimer ce que l'on compte faire :
public interface Assertion {
boolean isValid();
}
Étape 2 - Fournir une implémentation pour chaque use case :
Cas de la chaîne de caractères null
:
/**
* Cette classe prend un `Object` pour rendre générique le test de nullité.
*/
public final class NotNull implements Assertion {
private final Object value;
public NotNull(Object value) {
this.value = value;
}
@Override
public boolean isValid() {
return this.value != null;
}
}
Cas de la chaîne de caractères vide :
/**
* Il faudra fournir une implémentation de NotEmpty par type testé,
* ce qui peut sembler lourd mais qui a du sens puisque la notion de
* nullité peut changer en fonction du type. Par exemple, une String
* vide n'est pas la même chose qu'un tableau vide.
*/
public final class NotEmpty implements Assertion {
private final String value;
public NotEmpty(String value) {
this.value = value;
}
@Override
public boolean isValid() {
return !this.value.isEmpty();
}
}
Étape 3 - Ajout de la composition
L'assertion composée est elle aussi une implémentation de l'interface Assertion
et ne prend que des Assertion
en paramètre.
public final class Assertions implements Assertion {
private final Assertion[] assertions;
public Assertions(Assertion... assertions) {
this.assertions = assertions;
}
@Override
public boolean isValid() {
return Arrays.asList(this.assertions).stream().allMatch {
(Assertion it) -> is.isValid();
}
}
}
Étape 4 - Un exemple d'utilisation
public class Main {
public static void main(String... args) {
String toto = "titi";
new Assertions(
new NotNull(toto),
new NotEmpty(toto)
).isValid();
}
}
Dans cet exemple, aucun calcul n'est effectué dans le constructeur et tous les déclenchements sont joués uniquement au moment de l'invocation de la méthode isValid()
(on est donc en lazy evaluation), chaque objet possède un à état, tous les attributs sont immutables et les classes aussi (car final), il y a zéro héritage et uniquement de la composition, tout a été codé en interface-first pour s'abstraire des implémentations, enfin les classes étant rikiki, elles sont ultra-simples à tester.
Edit : en Kotlin le mot-clef new disparait car le langage assume qu'un constructeur soit une fonction static
à scope global (ce qui est le cas dans le bytecode de Java), du coup la chaîne d'instanciations apparaît comme une composition de fonctions (au sens f o g(x)
ou f(g(x))
) sans avoir besoin de passer par une fluent API et une monade.